﻿// Lightweight Job Scheduling Library for .NET
// Copyright (c) 2010 Philipp Sumi
// Contact and Information: http://www.hardcodet.net
//
// This library is free software; you can redistribute it and/or
// modify it in any way you see fit as long as this copyright
// notice is not being removed.
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE


using System;
using System.Collections.Generic;
using System.Threading;

namespace CS.Scheduling
{
    /// <summary>
    /// 提供简单的任务注册和运行的调度器。
    /// <para>Provides simple mechanisms to submit and run jobs.</para>
    /// </summary>
    public class Scheduler : IDisposable
    {
        /// <summary>
        /// 
        /// <para>The last interval that was assigned to the timer. This is used along with the <see cref="LastTimerRun"/> value to detect whether the system time was changed.</para>
        /// </summary>
        protected long LastTimerInterval { get; set; }

        /// <summary>
        /// 任务最后运行的时间戳。
        /// <para>A timestamp that marks the last timer run. Internally used along with the <see cref="LastTimerInterval"/> in order to detect system time changes.</para>
        /// </summary>
        protected DateTimeOffset LastTimerRun { get; private set; }

        /// <summary>
        /// 设置调度器是否为可释放。
        /// <para>Set to true as soon as the scheduler is disposed.</para>
        /// </summary>
        protected bool IsDisposed { get; set; }

        /// <summary>
        /// 
        /// <para>Indicates whether the execution time of jobs should be rescheduled in case a change of the system time was detected. This can avoid delays or untimely execution of jobs where no fixed execution times are desired, but rather an execution within a time span.</para>
        /// </summary>
        public ReschedulingStrategy SystemTimeChangeRescheduling { get; set; }

        /// <summary>
        /// 
        /// <para>An optional callback that handles exceptions that occur during the execution of scheduled jobs. The handler receives both the failing job and the exception that occurred.</para>
        /// </summary>
        public Action<Job, Exception> JobExceptionHandler { get; set; }
        
        private int _selfTestInterval = 120000;
        /// <summary>
        /// 
        /// <para>The minimum interval of the underlying timer in milliseconds. If the next job's planned execution time is above this interval,  the timer runs anyway in order to reschedule or run suddenly  pending jobs in case the system time was changed.<br/>Defaults to 120000 (2 minutes).</para>
        /// </summary>
        public int SelfTestInterval
        {
            get { return _selfTestInterval; }
            set
            {
                if (value < 100)
                {
                    const string msg = "Self testing interval must be greater than 100 milliseconds.";
                    throw new ArgumentOutOfRangeException("value", msg);
                }
                _selfTestInterval = value;
                //reschedule processing
                lock (SyncRoot)
                {
                    Reschedule();
                }

            }
        }

        private int _minJobInterval = 100;
        /// <summary>
        /// 任务执行最小间隔，默认100毫秒。
        /// <para>The minimum interval in milliseconds that is being used within the internal  job processing loop. Submitting a job with an interval  below this threshold causes an exception.<br/> Defaults to 100 milliseconds.</para>
        /// </summary>
        public int MinJobInterval
        {
            get { return _minJobInterval; }
            set
            {
                if (value < 1)
                {
                    const string msg = "Interval must be greater than zero milliseconds.";
                    throw new ArgumentOutOfRangeException("value", msg);
                }
                _minJobInterval = value;
            }
        }

        /// <summary>
        /// 内部计时器[Note:这是个全局计时器？]。
        /// <para>The internally used timer that is used to trigger execution of the individual callbacks.</para>
        /// </summary>
        protected Timer Timer { get; set; }

        /// <summary>
        /// 任务集合。
        /// <para>An internal list of maintained jobs. The list is sorted before processing.</para>
        /// </summary>
        protected List<JobContext> Jobs { get; set; }

        private readonly object _syncRoot = new object();
        /// <summary>
        /// 同步锁令牌。
        /// <para>The scheduler's locking token.</para>
        /// </summary>
        public object SyncRoot
        {
            get { return _syncRoot; }
        }

        /// <summary>
        /// 任务是否需要排序。
        /// <para>Indicates whether the internal job list needs sorting or not.</para>
        /// </summary>
        protected bool IsSorted { get; set; }

        /// <summary>
        /// 调度器的下次执行时间。
        /// <para>The time of the next execution.</para>
        /// </summary>
        public DateTimeOffset? NextExecution { get; protected set; }
        
        /// <summary>
        /// 构造一个调度器实例。
        /// <para>Creates a new scheduler instance.</para>
        /// </summary>
        public Scheduler()
        {
            Jobs = new List<JobContext>();
            Timer = new Timer(ProcessJobs, null, Timeout.Infinite, Timeout.Infinite);
        }
        
        /// <summary>
        /// 注册一个带回调的泛型任务。Todo:理解中...
        /// <para>Submits a generic job along with a callback that delivers the job's <see cref="Job{T}.Data"/>.</para>
        /// </summary>
        /// <typeparam name="T">
        /// 附加在任务上的泛型状态对象。
        /// <para> The type of the state object that can be attached to the job.</para>
        /// </typeparam>
        /// <param name="job">The job to be submitted.</param>
        /// <param name="callback">A callback action that is being invoked every time the job runs.</param>
        /// <exception cref="ArgumentNullException">If one of the parameters is a null reference.</exception>
        /// <exception cref="InvalidOperationException">If the configuration does not allow proper scheduling, e.g. because several loops were specified, but the inverval is missing.</exception>
        public virtual void SubmitJob<T>(Job<T> job, Action<Job<T>, T> callback)
        {
            //wrap action and forward
            Action<Job> action = j =>
            {
                var genericJob = (Job<T>)j;
                callback(genericJob, genericJob.Data);
            };

            SubmitJob(job, action);
        }

        /// <summary>
        /// 注册一个带回调的任务。
        /// <para>Submits a job to be executed.</para>
        /// </summary>
        /// <param name="job">The job to be submitted.</param>
        /// <param name="callback">A callback action that is being invoked every time the job runs.</param>
        /// <exception cref="ArgumentNullException">If one of the parameters is a null reference.</exception>
        /// <exception cref="InvalidOperationException">If the configuration does not allow proper scheduling, e.g. because several loops were specified, but the inverval is missing.</exception>
        public virtual void SubmitJob(Job job, Action<Job> callback)
        {
            if (job == null) throw new ArgumentNullException("job");
            if (callback == null) throw new ArgumentNullException("callback");

            //确保这是个有效的间隔
            //make sure we have a valid interval
            var interval = job.Interval;
            if (interval.HasValue && interval.Value.TotalMilliseconds < MinJobInterval)
            {
                var msg = "Interval of {0} ms is too small - a minimum interval of {1} ms is accepted.";
                msg = String.Format(msg, interval.Value.TotalMilliseconds, MinJobInterval);
                throw new InvalidOperationException(msg);
            }

            var context = new JobContext(job, callback);      //构造运行任务内容

            lock (SyncRoot)
            {
                //
                //if this job is going to be the next one, we need to reconfigure the timer. Do not reschedule if the next execution is imminent
                if (NextExecution == null || context.NextExecution <= NextExecution.Value)
                {
                    //新伤务先执行
                    //insert at index 0, which makes sure the new job runs first next timer event
                    Jobs.Insert(0, context);

                    //only reschedule if the next execution is not imminent
                    if (NextExecution == null || NextExecution.Value.Subtract(SystemTime.Now()).TotalMilliseconds > MinJobInterval)
                    {
                        //no sorting required, but we need to adjust the timer
                        Reschedule();
                    }
                }
                else
                {

                    //add at end of the list and mark list as unsorted the job will be sorted and rescheduled on the next run (which is before//this job's execution time)
                    Jobs.Add(context);
                    IsSorted = false;
                }
            }
        }


        /// <summary>
        /// 从调度器的内部缓存中获取任务。
        /// <para>Tries to get a job from the internal cache.</para>
        /// </summary>
        /// <param name="jobId">The job identifier.</param>
        /// <returns>The first job that matches the submitted ID, or null if no matching job was found.</returns>
        /// <remarks>Querying the scheduler for jobs might affect performance if done frequently.</remarks>
        public virtual Job TryGetJob(string jobId)
        {
            lock (SyncRoot)
            {
                //var context = Jobs.FirstOrDefault(jc => jc.ManagedJob.JobId == jobId); //3.5的写法
                var context = Jobs.Find(x => x.ManagedJob.JobId == jobId); //2.0的写法
                return context == null ? null : context.ManagedJob;
            }
        }

        
        /// <summary>
        /// 暂停一个特定的任务。
        /// <para>Pauses the current job.</para>
        /// </summary>
        /// <param name="jobId">The job identifier.</param>
        /// <returns>True if the job's <see cref="Job.State"/> was <see cref="JobState.Active"/> and was changed to <see cref="JobState.Paused"/>. False if the job was not found, or if the job's <see cref="Job.State"/> is not <see cref="JobState.Active"/>.</returns>
        /// <exception cref="InvalidOperationException">If the job has no  interval, and can thus not be rescheduled. For a job that runs
        /// just once, set the <see cref="Job.StartTime"/> accordingly.</exception>
        public virtual bool PauseJob(string jobId)
        {
            var job = TryGetJob(jobId);
            return job == null ? false : job.Pause();
        }


        /// <summary>
        /// Resumes a paused job.
        /// </summary>
        /// <param name="jobId">The job identifier.</param>
        /// <returns>True if the job's <see cref="Job.State"/>
        /// was <see cref="JobState.Paused"/> and was changed
        /// to <see cref="JobState.Active"/>. False if the job was not found, or if the job's
        /// <see cref="Job.State"/> is not <see cref="JobState.Paused"/>.</returns>
        public virtual bool ResumeJob(string jobId)
        {
            var job = TryGetJob(jobId);
            return job == null ? false : job.Resume();
        }



        /// <summary>
        /// Cancels a job with a given ID and removes it from the scheduler's internal
        /// cache.
        /// </summary>
        /// <param name="jobId">The job identifier.</param>
        /// <returns>True if a matching job was found and removed from
        /// the scheduler. False in case of an unknown job ID.</returns>
        public virtual bool CancelJob(string jobId)
        {
            lock (SyncRoot)
            {
                for (var i = 0; i < Jobs.Count; i++)
                {
                    var job = Jobs[i];
                    if (job.ManagedJob.JobId != jobId) continue;
                    Jobs.RemoveAt(i);
                    job.ManagedJob.Cancel();

                    //if we just removed the next job, reschedule
                    if (i == 0) Reschedule();

                    return true;
                }
            }

            return false;
        }


        /// <summary>
        /// Cancels all jobs.
        /// </summary>
        public virtual void CancelAll()
        {
            lock (SyncRoot)
            {
                if (IsDisposed) return;

                Timer.Change(Timeout.Infinite, Timeout.Infinite);
                NextExecution = null;
                Jobs.Clear();
            }
        }


        /// <summary>
        /// Timer event handler, which processes all currently
        /// active jobs.
        /// </summary>
        protected virtual void ProcessJobs(object state)
        {
            lock (SyncRoot)
            {
                if (IsDisposed) return;

                if (SystemTimeChangeRescheduling != ReschedulingStrategy.KeepFixedTimes)
                {
                    VerifySystemTime();
                }

                //run and execute jobs
                RunPendingJobs();

                //if the list is not sorted, do so now to have the most
                //pending job at index 0
                if (!IsSorted) SortJobs();

                //plan the next execution
                Reschedule();
            }
        }


        /// <summary>
        /// Verifies the system time was not changed. If a changed time was detected, all jobs are being rescheduled in order to maintain the relative execution time.
        /// </summary>
        protected virtual void VerifySystemTime()
        {
            //we didn't have an interval at all
            if (LastTimerInterval == Timeout.Infinite) return;

            //get the time between now and the last timer we sent the timer to sleep
            var now = SystemTime.Now();
            var pauseDuration = now.Subtract(LastTimerRun);

            //subtract the timer's sleeping time - this value should be close to 0
            var delta = pauseDuration.TotalMilliseconds - LastTimerInterval;

            //the difference between the current time and the last execution
            //should roughly match the timer interval
            if (delta > 1000 || delta < 1000)
            {
                //reschedule all jobs
                var changeExpirationTime = SystemTimeChangeRescheduling ==
                                           ReschedulingStrategy.RescheduleNextExecutionAndExpirationTime;
                Jobs.ForEach(jc =>
                {
                    jc.NextExecution = jc.NextExecution.Value.AddMilliseconds(delta);
                    if (changeExpirationTime && jc.ManagedJob.ExpirationTime.HasValue)
                    {
                        jc.ManagedJob.ExpirationTime = jc.ManagedJob.ExpirationTime.Value.AddMilliseconds(delta);
                    }
                });
            }

        }


        /// <summary>
        /// Sorts the job list. This method does not perform any locking, this must
        /// be done by the invoking party.
        /// </summary>
        protected virtual void SortJobs()
        {
            Jobs.Sort((first, second) => first.NextExecution.Value.CompareTo(second.NextExecution.Value));
            IsSorted = true;
        }




        /// <summary>
        /// Processes all currently pending jobs. If a job is not
        /// supposed to run anymore, it is being removed from the
        /// internal cache.
        /// </summary>
        protected virtual void RunPendingJobs()
        {
            //remove all jobs 
            var now = SystemTime.Now();
            var dueJobs = new List<JobContext>();

            for (var i = 0; i < Jobs.Count; i++)
            {
                var job = Jobs[i];

                //if the job is not ready yet, break
                if (job.NextExecution.Value > now) break;

                //add to list of due jobs
                dueJobs.Add(job);
            }

            JobContext currentJob = null;
            try
            {
                for (var i = dueJobs.Count - 1; i >= 0; i--)
                {
                    currentJob = dueJobs[i];

                    //execute jobs
                    currentJob.ExecuteAsync(this);

                    //if the job will run again, set the IsSorted flag to false in order
                    //to move the job to its new position in the list
                    if (currentJob.NextExecution.HasValue)
                    {
                        IsSorted = false;
                    }
                    else
                    {
                        //remove the job from the job list
                        Jobs.RemoveAt(i);
                    }
                }
            }
            catch (Exception e)
            {
                // ReSharper disable PossibleNullReferenceException
                if (!SubmitJobException(currentJob.ManagedJob, e)) throw;
                // ReSharper restore PossibleNullReferenceException
            }
        }





        /// <summary>
        /// 
        /// <para>Reconfigures the timer according to the next pending job execution time.</para>
        /// </summary>
        protected virtual void Reschedule()
        {
            if (IsDisposed) return;

            if (Jobs.Count == 0)
            {
                //disable the timer if we don't have any pending jobs
                NextExecution = null;
                Timer.Change(Timeout.Infinite, Timeout.Infinite);
                LastTimerInterval = Timeout.Infinite;
            }
            else
            {
                //schedule next event
                var executionTime = Jobs[0].NextExecution;

                var now = SystemTime.Now();
                var delay = executionTime.Value.Subtract(now);

                //in case the next execution is already pending, add a safe delay
                var dueTime = Math.Max(MinJobInterval, (long)delay.TotalMilliseconds);

                //change the timer - run at least with the self testing interval
                dueTime = Math.Min(dueTime, SelfTestInterval);

                NextExecution = SystemTime.Now().AddMilliseconds(dueTime);
                Timer.Change(dueTime, Timeout.Infinite);
                LastTimerInterval = dueTime;
            }

            LastTimerRun = SystemTime.Now();
        }


        /// <summary>
        /// Performs application-defined tasks associated with freeing,
        /// releasing, or resetting unmanaged resources.
        /// </summary>
        public virtual void Dispose()
        {
            lock (SyncRoot)
            {
                if (IsDisposed) return;

                IsDisposed = true;
                Timer.Change(Timeout.Infinite, Timeout.Infinite);
                Timer.Dispose();
            }
        }


        /// <summary>
        /// Forwards an exception to a registered <see cref="JobExceptionHandler"/>,
        /// if possible.
        /// </summary>
        /// <param name="job">The currently processed job. May be null.</param>
        /// <param name="exception">The exception that occurred.</param>
        /// <returns>True if the exception was forwarded to a registered
        /// <see cref="JobExceptionHandler"/>. False if no callback handler
        /// was registered.</returns>
        internal virtual bool SubmitJobException(Job job, Exception exception)
        {
            var handler = JobExceptionHandler;
            if (handler != null)
            {
                handler(job, exception);
                return true;
            }

            return false;
        }

    }
}
